﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace Vcm
{
    public abstract class CardMakerBase : ICardMaker
    {
        public virtual float MaxTextFontSize
        {
            get { return 9f; }
        }

        private static float _zoomFactor = 1f;
        public static float ZoomFactor
        {
            get { return _zoomFactor; }
            set { _zoomFactor = value; }
        }

        protected static readonly string ResourcesPath;
        protected const float Inch = 72; // number of points per inch
        protected const float CardWidthBorder = 2.3225f * Inch * 1.075f; //1.0388f;
        protected const float CardHeightBorder = 3.307f * Inch * 1.05355f; //1.0683f;
        public const float CardWidth = (58f / 63f) * CardWidthBorder;
        public const float CardHeight = (82.5f / 88f) * CardHeightBorder;

        public const float LeftPageMargin = 0.5f * Inch;
        public const float BottomPageMargin = 0.5f * Inch;
        protected static readonly BaseFont SymbolFont;
        //protected static readonly BaseFont ClanFont1;
        protected static readonly BaseFont TextFont;
        protected static readonly BaseFont BoldTextFont;
        protected static readonly BaseFont ItalicTextFont;
        protected static readonly BaseFont BoldItalicTextFont;
        protected static readonly BaseFont CondensedFont;
        protected const float SymbolSuperpositionFactor = 3f; // used by convictions
        protected const string InferiorDiscipline = "<";
        protected const string SuperiorDiscipline = ">";
        protected const float BaselineFactor = 0.8f; // used to remove baseline from the font size
        protected const int HighlightColor = 0xFF0000;

        protected static string[] ImageExtensions = new[] { "gif", "png", "jpg" };

        protected class DisciplineInfo
        {
            public readonly string Text;
            public readonly string Name;
            public DisciplineInfo(string text, string name)
            {
                Text = text;
                Name = name;
            }
        }

        protected static readonly Dictionary<string, DisciplineInfo> Disciplines = new Dictionary<string, DisciplineInfo>(StringComparer.Ordinal)
        {
            { "[aus]", new DisciplineInfo("a", "Auspex") },
            { "[obe]", new DisciplineInfo("b", "Obeah") },
            { "[cel]", new DisciplineInfo("c", "Celerity") },
            { "[dom]", new DisciplineInfo("d", "Dominate") },
            { "[dem]", new DisciplineInfo("e", "Dementation") },
            { "[for]", new DisciplineInfo("f", "Fortitude") },
            { "[san]", new DisciplineInfo("g", "Sanguinus") },
            { "[thn]", new DisciplineInfo("h", "Thanatosis") },
            { "[ani]", new DisciplineInfo("i", "Animalism") },
            { "[pro]", new DisciplineInfo("j", "Protean") },
            { "[chi]", new DisciplineInfo("k", "Chimerstry") },
            { "[val]", new DisciplineInfo("l", "Valeren") },
            { "[mel]", new DisciplineInfo("m", "Melpominee") },
            { "[nec]", new DisciplineInfo("n", "Necromancy") },
            { "[obf]", new DisciplineInfo("o", "Obfuscate") },
            { "[pot]", new DisciplineInfo("p", "Potence") },
            { "[qui]", new DisciplineInfo("q", "Quietus") },
            { "[pre]", new DisciplineInfo("r", "Presence") },
            { "[ser]", new DisciplineInfo("s", "Serpentis") },
            { "[tha]", new DisciplineInfo("t", "Thaumaturgy") },
            { "[vis]", new DisciplineInfo("u", "Visceratika") },
            { "[vic]", new DisciplineInfo("v", "Vicissitude") },
            { "[abo]", new DisciplineInfo("w", "Abombwe") },
            { "[myt]", new DisciplineInfo("x", "Mytherceria") },
            { "[dai]", new DisciplineInfo("y", "Daimoinon") },
            { "[spi]", new DisciplineInfo("z", "Spiritus") },
            { "[tem]", new DisciplineInfo("?", "Temporis") },
            { "[TEM]", new DisciplineInfo("!", "Temporis") },
            { "[obt]", new DisciplineInfo("$", "Obtenebration") },
            { "[OBT]", new DisciplineInfo("£", "Obtenebration") },
            { string.Empty, new DisciplineInfo("`^", "Flight")},
            // TODO: maleficia & striga
        };

        protected class ClanSymbolInfo
        {
            public readonly float WidthFactor;
            public readonly bool IsRotated;
            public readonly bool ShowOnCryptCard;

            public ClanSymbolInfo(float widthFactor, bool isRotated = false, bool showOnCryptCard = true)
            {
                WidthFactor = widthFactor;
                IsRotated = isRotated;
                ShowOnCryptCard = showOnCryptCard;
            }
        }

        protected static readonly ClanSymbolInfo DefaultClanSymbolInfo = new ClanSymbolInfo(OtherClanFactor);

        protected const float RedSealClanFactor = 0.19f;
        protected const float OtherClanFactor = 0.11f;
        protected static readonly Dictionary<string, ClanSymbolInfo> ClanWidthFactors = new Dictionary<string, ClanSymbolInfo> 
        {
            {"Abomination", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Ahrimane", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)}, // checked
            {"Akunanse", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Assamite", new ClanSymbolInfo(OtherClanFactor, true)}, // checked
            {"Baali", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Blood Brother", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)}, // checked
            {"Brujah", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Brujah antitribu", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)},
            {"Caitiff", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Gangrel", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Gangrel antitribu", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)},
            {"Guruhi", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Ishtarri", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Malkavian", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Harbinger of Skulls", new ClanSymbolInfo(RedSealClanFactor)}, // checked
            {"Kiasyd", new ClanSymbolInfo(RedSealClanFactor)}, // checked
            {"Lasombra", new ClanSymbolInfo(RedSealClanFactor)},
            {"Malkavian antitribu", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)},
            {"Nosferatu", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Nosferatu antitribu", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)},
            {"Osebo", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Pander", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)},
            {"Salubri antitribu", new ClanSymbolInfo(RedSealClanFactor)}, // checked
            {"Toreador", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Toreador antitribu", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)},
            {"Tremere", new ClanSymbolInfo(OtherClanFactor, showOnCryptCard: false)},
            {"Tremere antitribu", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)},
            {"Tzimisce", new ClanSymbolInfo(RedSealClanFactor)},
            {"Ventrue antitribu", new ClanSymbolInfo(RedSealClanFactor, showOnCryptCard: false)}, // checked
        };

        protected class IconInfo
        {
            public readonly string Text;
            public bool IsAlsoAction;
            public bool Superpose;
            public string AdditionalText;
            public bool RequiresFollowingUnbreakableSpace;
            public bool RequiresPrecedingUnbreakableSpace = true;
            public float ScaleFactor = 1f;
            public float VerticalOffset;
            public IconInfo(string text)
            {
                Text = text;
            }
        }

        protected static readonly Dictionary<string, IconInfo> Icons = new Dictionary<string, IconInfo>(StringComparer.OrdinalIgnoreCase)
        {
            { "[ACTION]", new IconInfo("<0")},
            { "[COMBAT]", new IconInfo("<4")},
            { "[ACTION MODIFIER]", new IconInfo("<1")},
            { "[REFLEX]", new IconInfo("<6")},
            { "[REACTION]", new IconInfo("<7")},
            { "[POLITICAL ACTION]", new IconInfo("<2") { IsAlsoAction = true, AdditionalText = "1 Vote" }},
            { "[ALLY]", new IconInfo("<3") { IsAlsoAction = true }},
            { "[EQUIPMENT]", new IconInfo("<5") { IsAlsoAction = true }},
            { "[RETAINER]", new IconInfo("<8") { IsAlsoAction = true }},
            { "[POWER]", new IconInfo("<3") { IsAlsoAction = true }}, // todo: add power icon (currently we display the ally icon)
            { "(D)", new IconInfo("{}") { RequiresPrecedingUnbreakableSpace = false, RequiresFollowingUnbreakableSpace = true }},
            { "[PARADOX]", new IconInfo("§9") { RequiresPrecedingUnbreakableSpace = false }},
            { "[1 PARADOX]", new IconInfo("§9") },
            { "[2 PARADOX]", new IconInfo("§9§9") },
            { "[3 PARADOX]", new IconInfo("§9§9§9") },
            { "[CONVICTION]", new IconInfo("¥¤")},
            { "[1 CONVICTION]", new IconInfo("¥¤")},
            { "[2 CONVICTION]", new IconInfo("¥¤¥¤") { Superpose = true }},
            { "[3 CONVICTION]", new IconInfo("¥¤¥¤¥¤"){ Superpose = true }},
            { "[4 CONVICTION]", new IconInfo("¥¤¥¤¥¤¥¤"){ Superpose = true }},
            { "[5 CONVICTION]", new IconInfo("¥¤¥¤¥¤¥¤¥¤"){ Superpose = true }},
            { "[FLIGHT]", new IconInfo("`^")},
            { "[MERGED]", new IconInfo("¶µ")},
            { "[BURN OPTION]", new IconInfo("<~")},
            { "[EVENT]", new IconInfo("][") { ScaleFactor = 1.5f, VerticalOffset = -0.49f} },
        };

        protected static readonly string[] ClanNames = new[] { "Abomination", "Abominations", "Ahrimane", "Ahrimanes", "Akunanse", "Assamite", "Assamites", "Avenger", "Avengers", "Baali", "Blood Brother", "Blood Brothers", "Brujah", "Brujah antitribu", "Caitiff", "Daughter of Cacophony", "Daughters of Cacophony", "Defender", "Defenders", "Follower of Set", "Followers of Set", "Gangrel", "Gangrel antitribu", "Gargoyle", "Gargoyles", "Giovanni", "Guruhi", "Harbinger of Skulls", "Harbinger of Skulls", "Innocent", "Innocents", "Ishtarri", "Judge", "Judges", "Kiasyd", "Lasombra", "Malkavian", "Malkavians", "Malkavian antitribu", "Martyr", "Martyrs", "Nagaraja", "Nosferatu", "Nosferatu antitribu", "Osebo", "Pander", "Ravnos", "Redeemer", "Redeemers", "Salubri", "Salubri antitribu", "Samedi", "Toreador", "Toreador antitribu", "Tremere", "Tremere antitribu", "True Brujah", "Tzimisce", "Ventrue", "Ventrue antitribu", "Visionary", "Visionaries" };

        protected static readonly List<string> EncounteredClanNames = new List<string>();
        protected static string CardName = string.Empty;

        static CardMakerBase()
        {
            ResourcesPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + @"\Resources\";
            SymbolFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\Ankha VTES Font.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);
            //ClanFont1 = BaseFont.CreateFont(ResourcesPath + @"Fonts\WOD_____.TTF", BaseFont.CP1252, BaseFont.EMBEDDED);
            TextFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\GIL_____.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);
            BoldTextFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\GILB___0.TTF", BaseFont.CP1252, BaseFont.EMBEDDED);
            ItalicTextFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\GILI___0.TTF", BaseFont.CP1252, BaseFont.EMBEDDED);
            CondensedFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\glsnecb.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);
            BoldItalicTextFont = BaseFont.CreateFont(ResourcesPath + @"Fonts\Gill Sans MT Bold Italic.ttf", BaseFont.CP1252, BaseFont.EMBEDDED);
        }

        public virtual void AddCard(ProxyDocument proxyDocument, DataRow cardRow)
        {
            string dummy;
            AddCard(proxyDocument, cardRow, out dummy);
        }

        public abstract void AddCard(ProxyDocument proxyDocument, DataRow cardRow, out string outputMessage);
        public abstract bool SupportsRow(DataRow cardRow);

        protected static string NormalizeImageName(string name)
        {
            var invalidChars = Path.GetInvalidFileNameChars().Where(c => ((byte)c) > 32).ToList();
            var charName = name.ToArray();
            for (int i = 0; i < charName.Length; ++i)
                if (invalidChars.Contains(charName[i]))
                    charName[i] = '_';
            return new string(charName);
        }

        protected static void WriteName(Document doc, PdfContentByte cb, string name,
            float x, float y, float width, float height,
            BaseFont[] fonts, int foreColor)
        {
            const float nameScaling = 109f;
            WriteOneLine(doc, cb, name, x, y, width, height, fonts, foreColor, 0x00000, false, nameScaling);
        }

        protected static void WriteOneLine(Document doc, PdfContentByte cb, string text,
            float x, float y, float width, float height,
            BaseFont[] fonts, int foreColor, int? outlineColor, bool isCentered, float scaling = 100f)
        {
            //cb.Rectangle(x, y, width, height);
            //cb.Stroke();

            float currentHeight;
            int lineCount = 0;
            var segments = GetSegments(text, ClanIconsDisplay.IgnoreAll);

            var fontSize = height;
            bool isFitting = false;
            var segmentGroups = new[] { new SegmentGroup(segments) };
            while (!isFitting && fontSize > 0)
            {
                currentHeight = 0f;
                isFitting = AppendOrWriteSegments(null, null, segmentGroups, x, y, width, ref currentHeight, fonts, fontSize, isCentered, foreColor, outlineColor, new List<float>(), ref lineCount, false, exclusionAreaFuncs: null, scaling: scaling)
                    && currentHeight <= height;

                if (!isFitting)
                    fontSize -= 0.1f;
            }
            if (!isFitting)
                throw new Exception("Can't make all the text fit: " + text);

            // perform the rendering
            currentHeight = 0f;
            lineCount = 0;
            AppendOrWriteSegments(doc, cb, segmentGroups, x, y, width, ref currentHeight, fonts, fontSize, isCentered, foreColor, outlineColor, new List<float>(), ref lineCount, false, exclusionAreaFuncs: null, scaling: scaling);
        }

        /// <summary>
        /// Group segment so symbol and single characters are kept with the previous segment.
        /// </summary>
        /// <param name="segments"></param>
        /// <returns></returns>
        protected static IList<SegmentGroup> GroupSegments(IList<Segment> segments)
        {
            var result = new List<SegmentGroup>();

            var newGroup = true;
            SegmentGroup group = null;
            for (int i = 0; i < segments.Count; ++i)
            {
                if (newGroup)
                {
                    group = new SegmentGroup();
                    result.Add(group);
                }
                if (segments[i].Text.Length > 0)
                {
                    char lastChar;
                    group.Add(segments[i]);
                    newGroup = i < segments.Count - 1 && !(segments[i + 1].RequiresPrecedingUnbreakableSpace)
                        && !segments[i].RequiresFollowingUnbreakableSpace
                        && !(segments[i].IsSymbol && i < segments.Count - 1 && segments[i + 1].Text.Length > 0 && IsPunctuation(segments[i + 1].Text[0])) // symbol followed by a punctuation mark
                        && !(IsDigit(lastChar = segments[i].Text[segments[i].Text.Length - 1]) || (lastChar == ':' && !segments[i].NoUnbreakableSpaceAfterColumn))
                        && !IsFollowedByUnbreakableSpace(segments[i].Text);
                }
                else
                {
                    newGroup = false;
                }
            }
            return result;
        }

        protected static bool IsFollowedByUnbreakableSpace(string word)
        {
            return string.Equals(word, "unique", StringComparison.InvariantCultureIgnoreCase);
        }

        protected static bool IsDigit(char c)
        {
            return (c >= '0' && c <= '9') || (c == 'X');
        }

        protected static bool IsPunctuation(char c)
        {
            return c == '.' || c == ',' || c == ';' || c == '!' || c == '?' || c == ':' || c == '(' || c == ')';
        }

        protected static string TrimPunctuation(string text)
        {
            var startIndex = 0;
            var endIndex = text.Length - 1;

            while (startIndex <= endIndex && IsPunctuation(text[startIndex]))
                ++startIndex;
            while (endIndex > startIndex && IsPunctuation(text[endIndex]))
                --endIndex;

            if (startIndex == 0 && endIndex == text.Length - 1)
                return text;

            if (startIndex == endIndex)
                return string.Empty;
            return text.Substring(startIndex, endIndex - startIndex + 1);
        }

        protected static BaseFont GetFont(BaseFont[] fonts, Segment segment)
        {
            if (segment.IsSymbol)
                return fonts[(int)FontType.Symbol];
            if (segment.IsBold)
                return segment.IsItalic ? fonts[(int)FontType.BoldItalic] : fonts[(int)FontType.Bold];
            if (segment.IsItalic)
                return fonts[(int)FontType.Italic];
            if (segment.IsCondensed)
                return fonts[(int)FontType.Condensed];
            return fonts[(int)FontType.Regular];
        }

        private class Size
        {
            public readonly float Width, Height;

            public Size(float width, float height)
            {
                Width = width;
                Height = height;
            }
        }

        private static readonly Dictionary<string, Size> _iconSizes = new Dictionary<string, Size>();

        protected static bool RequiresWhitespacePrefix(string text)
        {
            return text.Length > 1 || !IsPunctuation(text[0]);
        }

        protected static float GetSegmentWidthOrWriteSegment(Document doc, PdfContentByte cb, Segment segment,
            float x, float y,
            BaseFont font, float fontSize, int foreColor,
            int? outlineColor = null, float scaling = 100f)
        {
            var color = segment.IsHighlighted ? HighlightColor : foreColor;
            if (segment.IsSymbol)
            {
                var offset = 0f;
                for (var j = 0; j < segment.Text.Length / 2; ++j)
                {
                    if (cb != null)
                        WriteSymbol(cb, x + offset, y + (1f - BaselineFactor) * fontSize, font, fontSize, segment.Text[j * 2], segment.Text[j * 2 + 1], color);
                    var width = font.GetWidthPoint(segment.Text[j * 2 + 1], fontSize); // no scaling on icons
                    if (segment.Superpose && j + 1 < segment.Text.Length / 2)
                        width /= SymbolSuperpositionFactor;
                    offset += width;
                }
                return offset;
            }
            if (segment.IsIcon)
            {
                Size size;
                if (!_iconSizes.ContainsKey(segment.Text) || doc != null)
                {
                    var icon = ImageProvider.GetFittingImage("bitmapIcon-" + segment.Text, string.Format(@"{0}Images\ClanIcon-{1}", ResourcesPath, segment.Text), ImageExtensions, 1000f, fontSize)
                        ?? ImageProvider.GetFittingImage("bitmapSymbol-" + segment.Text, string.Format(@"{0}Images\ClanSymbol-{1}", ResourcesPath, segment.Text), ImageExtensions, 1000f, fontSize);
                    size = icon != null ? new Size(icon.Width, icon.Height) : null;
                    if (doc != null && icon != null)
                    {
                        icon.SetAbsolutePosition(x, y);
                        doc.Add(icon);
                    }
                    else
                    {
                        _iconSizes.Add(segment.Text, size);
                    }
                }
                else
                {
                    size = _iconSizes[segment.Text];
                }
                return size == null ? 0 : size.Width * fontSize / size.Height;
            }
            if (cb != null)
            {
                if (outlineColor.HasValue)
                    WriteText(cb, segment.Text, x, y + (1f - BaselineFactor) * fontSize, font, fontSize, color, outlineColor.Value, scaling);
                else
                    WriteText(cb, segment.Text, x, y + (1f - BaselineFactor) * fontSize, font, fontSize, color, scaling);
            }
            return font.GetWidthPoint(segment.Text, fontSize) * scaling / 100f;
        }

        public struct RenderData
        {
            public Segment Segment;
            public float X;
            public float Y;

            public RenderData(Segment segment, float x, float y)
            {
                this.Segment = segment;
                this.X = x;
                this.Y = y;
            }
        }

        public static bool AppendOrWriteSegments(Document doc, PdfContentByte cb, IList<SegmentGroup> segmentGroups,
            float x, float y, float maxWidth, ref float currentHeight,
            BaseFont[] fonts, float fontSize, bool isCentered, int foreColor, int? outlineColor, List<float> segmentWidths, ref int lineCount,
            bool renderBackgroundOnly,
            ICollection<Func<Rectangle, bool, bool>> exclusionAreaFuncs = null, float scaling = 100f)
        {
            var renderData = new List<RenderData>();
            var drawBackground = segmentGroups[0][0].DrawBackground;

            if (drawBackground)
                currentHeight += fontSize / 2;

            var firstSegmentOfLine = true;
            float currentX = 0f;
            var index = 0;
            var positions = new List<float>();
            while (index < segmentGroups.Count)
            {
                var segmentWidth = 0f;
                positions.Clear();
                var segmentGroup = segmentGroups[index];
                foreach (var segment in segmentGroup)
                {
                    var segmentFont = GetFont(fonts, segment);
                    if (!firstSegmentOfLine && RequiresWhitespacePrefix(segment.Text))
                        segmentWidth += segmentFont.GetWidthPoint(" ", fontSize) * scaling / 100f;
                    positions.Add(currentX + segmentWidth);
                    segmentWidth += GetSegmentWidthOrWriteSegment(null, null, segment, 0, 0, segmentFont, fontSize, foreColor, outlineColor, scaling);
                    firstSegmentOfLine = false;
                }

                Rectangle segmentRect = null;
                if (exclusionAreaFuncs != null)
                {
                    var centeringOffset = isCentered ? (maxWidth - (currentX + segmentWidth)) / 2f : 0f;
                    segmentRect = new Rectangle(x + currentX + centeringOffset, y - currentHeight, x + currentX + segmentWidth + centeringOffset, y - currentHeight + fontSize);
                }

                if (currentX + segmentWidth <= maxWidth && (exclusionAreaFuncs == null || !exclusionAreaFuncs.Any(f => f(segmentRect, isCentered))))
                {
                    if (doc != null && cb != null)
                    {
                        for (int i = 0; i < segmentGroup.Count; ++i)
                            if (isCentered)
                                renderData.Add(new RenderData(segmentGroup[i], x + positions[i], y - currentHeight));
                            else
                                GetSegmentWidthOrWriteSegment(doc, cb, segmentGroup[i], x + positions[i], y - currentHeight, GetFont(fonts, segmentGroup[i]), fontSize, foreColor, outlineColor, scaling);
                    }
                    currentX += segmentWidth;
                    firstSegmentOfLine = false;
                    ++index;
                }
                else if (currentX == 0f) // segment group doesn't fit
                {
                    return false;
                }
                else
                {
                    if (segmentWidths != null)
                        segmentWidths.Add(currentX);
                    currentX = 0f;
                    firstSegmentOfLine = true;
                    currentHeight += fontSize;
                    ++lineCount;
                }
            }
            if (currentX > 0)
            {
                currentHeight += fontSize;
                ++lineCount;
                if (segmentWidths != null)
                    segmentWidths.Add(currentX);
            }

            if (doc != null && cb != null && segmentWidths != null && isCentered)
            {
                // render from the positions 
                var renderDataByY = renderData.GroupBy(p => p.Y).ToList();
                var startIndex = segmentWidths.Count - renderDataByY.Count; // segmentWidths contains all the segmentGroups items, whereas renderDataByY contains only the latest segmentGroups item.

                if (renderBackgroundOnly && drawBackground)
                {
                    var gstate = new PdfGState { FillOpacity = 0.5f };
                    cb.SaveState();
                    cb.SetGState(gstate);

                    cb.SetRGBColorFill(255, 255, 255);
                    cb.RoundRectangle(x - 2f, renderDataByY.Last().Key, maxWidth + 4f, y - renderDataByY.Last().Key + fontSize / 2f, fontSize / 2f);
                    cb.Fill();
                    cb.RestoreState();  
                }

                if (renderBackgroundOnly)
                    return true;

                for (int i = 0; i < renderDataByY.Count; ++i)
                {
                    var group = renderDataByY[i];
                    var width = segmentWidths[startIndex + i];
                    var offset = (maxWidth - width) / 2f;

                    foreach (var renderDataItem in group)
                    {
                        GetSegmentWidthOrWriteSegment(doc, cb, renderDataItem.Segment, offset + renderDataItem.X, renderDataItem.Y, GetFont(fonts, renderDataItem.Segment), fontSize, foreColor, outlineColor, scaling);
                    }
                }
            }
            return true;
        }

        protected enum ClanIconsDisplay
        {
            DisplayFirst,
            IgnoreAll,
            DisplayAll,
        }

        protected static IList<Segment> GetSegments(string text, ClanIconsDisplay clanIconsDisplay = ClanIconsDisplay.DisplayFirst, bool italicizedQuotes = false)
        {
            bool dummy = false;
            return GetSegments(text, ref dummy, clanIconsDisplay, italicizedQuotes);
        }

        protected static IList<Segment> GetSegments(string text, ref bool isHighlighted, ClanIconsDisplay clanIconsDisplay = ClanIconsDisplay.DisplayFirst, bool italicizedQuotes = false)
        {
            var result = new List<Segment>();

            text = text.Replace("Black Hand", "Black¤Hand");
            text = text.Replace("stealth action.", "stealth¤action.");
            for (int i = 1; i <= 6; ++i)
                text = text.Replace(string.Format("a +{0} stealth¤action.", i), string.Format("a +{0} stealth action.", i));

            if (italicizedQuotes)
            {
                var quoteIndices = new List<int>();
                for (var i = 0; i < text.Length; ++i)
                    if (text[i] == '"')
                        quoteIndices.Add(i);
                var textBuilder = new StringBuilder(text);
                for (var i = 0; i < quoteIndices.Count / 2; ++i)
                {
                    var closingQuoteIndex = quoteIndices[i * 2 + 1];
                    if (closingQuoteIndex == text.Length - 1 || text[closingQuoteIndex + 1] == ','
                        || closingQuoteIndex < text.Length - 2 && text[closingQuoteIndex + 2] == '(') // opus title
                    {
                        var openingQuoteIndex = quoteIndices[i * 2];
                        textBuilder[openingQuoteIndex] = '|';
                        textBuilder[closingQuoteIndex] = '|';
                    }
                }
                text = textBuilder.ToString();
            }

            var words = text.Split(' ').Where(w => !string.IsNullOrEmpty(w)).ToArray();
            if (words.Length == 0)
                return result;

            // add clan and discipline symbols
            var newWords = AddClanSymbols(words);

            words = SplittedStringHelper.RegroupItems(newWords, ' ', '[', ']', false)
                .Select(w => w.Replace("¤", " "))
                .ToArray();

            bool isItalicized = false;
            foreach (var word in words)
            {
                Segment currentSegment;
                DisciplineInfo disciplineInfo;
                IconInfo symbolInfos;

                if (word.StartsWith("@"))
                    isHighlighted ^= true;
                bool mustToggleHighlight = word.LastIndexOf('@') > 0;
                var currentWord = word.Replace("@", null);

                if (currentWord.StartsWith("~"))
                    isItalicized ^= true;
                bool mustToggleItalicize = currentWord.LastIndexOf('~') > 0;
                currentWord = currentWord.Replace("~", null);

                if (italicizedQuotes)
                {
                    if (currentWord[0] == '|')
                        isItalicized ^= true;
                    mustToggleItalicize = currentWord.LastIndexOf('|') > 0;
                    currentWord = currentWord.Replace("|", "");
                }

                string clanName;

                if (currentWord.Length == 1 && IsPunctuation(currentWord[0])) // happens if the punctuation follows a ']'
                {
                    currentSegment = new Segment { Text = currentWord, RequiresPrecedingUnbreakableSpace = true };
                }
                else if (Disciplines.TryGetValue(currentWord, out disciplineInfo))
                    currentSegment = new Segment
                    {
                        Text = (IsSuperiorDiscipline(currentWord) ? SuperiorDiscipline : InferiorDiscipline) + disciplineInfo.Text,
                        IsSymbol = true,
                        RequiresPrecedingUnbreakableSpace = true
                    };
                else if (Disciplines.TryGetValue(currentWord.ToLower(), out disciplineInfo))
                    currentSegment = new Segment { Text = SuperiorDiscipline + disciplineInfo.Text.ToUpper(), IsSymbol = true };
                else if (Icons.TryGetValue(currentWord, out symbolInfos))
                    currentSegment = new Segment
                    {
                        Text = symbolInfos.Text,
                        IsSymbol = true,
                        Superpose = symbolInfos.Superpose,
                        RequiresPrecedingUnbreakableSpace = symbolInfos.RequiresPrecedingUnbreakableSpace,
                        RequiresFollowingUnbreakableSpace = symbolInfos.RequiresFollowingUnbreakableSpace
                    };
                else if (currentWord.Length > 2 && currentWord[0] == '[' && currentWord[currentWord.Length - 1] == ']' && ClanNames.Contains(clanName = currentWord.Substring(1, currentWord.Length - 2)))
                {
                    if (clanIconsDisplay == ClanIconsDisplay.IgnoreAll
                        || (clanIconsDisplay == ClanIconsDisplay.DisplayFirst && EncounteredClanNames.Contains(clanName)))
                        currentSegment = null; // do not show the icon again
                    else
                    {
                        currentSegment = new Segment { Text = clanName, IsIcon = true, RequiresPrecedingUnbreakableSpace = true };
                        EncounteredClanNames.Add(clanName);
                    }
                }
                else
                    currentSegment = new Segment { Text = currentWord };

                if (currentSegment != null)
                {
                    currentSegment.IsHighlighted = isHighlighted;
                    if (isItalicized)
                        currentSegment.ToggleItalics();
                    result.Add(currentSegment);
                }

                if (mustToggleHighlight)
                    isHighlighted ^= true;
                if (mustToggleItalicize)
                    isItalicized ^= true;
            }
            return result;
        }

        private static bool IsClanTitle(string[] words, int startIndex)
        {
            return startIndex < words.Length && (words[startIndex].StartsWith("Justicar") || words[startIndex].StartsWith("Kholo"))
                || startIndex < words.Length - 1 && words[startIndex] == "Inner" && words[startIndex + 1].StartsWith("Circle");
        }

        private static List<string> AddClanSymbols(string[] words)
        {
            var result = new List<string>(words.Length);
            var cardNameWords = CardName.Split(' ');
            var cleanCardNameWords = cardNameWords.Select(w => w.Replace("@", "").Replace("~", "")).ToArray();

            var cleanWords = words.Select(w => w.Replace("@", "").Replace("~", "")).ToArray();
            for (int i = 0; i < cleanWords.Length; ++i)
            {
                if (i + cleanCardNameWords.Length <= cleanWords.Length)
                {
                    var matches = false;
                    for (var j = 0; j < cleanCardNameWords.Length; ++j)
                    {
                        if (!string.Equals(cleanWords[i + j], cleanCardNameWords[j]))
                            break;
                        if (j == cleanCardNameWords.Length - 1)
                            matches = true;
                    }
                    if (matches)
                    {
                        result.AddRange(cardNameWords);
                        i += cleanCardNameWords.Length - 1;
                        continue;
                    }
                }

                int clanNameIndex;
                if (i < cleanWords.Length - 2)
                {
                    var threeWords = TrimPunctuation(cleanWords[i] + " " + cleanWords[i + 1] + " " + cleanWords[i + 2]);
                    if (threeWords.EndsWith("'s"))
                        threeWords = threeWords.Substring(0, threeWords.Length - 2);
                    if (-1 != (clanNameIndex = Array.IndexOf(ClanNames, threeWords)) && !IsClanTitle(cleanWords, i + 3))
                    {
                        if (clanNameIndex > 0
                            && ClanNames[clanNameIndex - 1].Substring(0, 4) == ClanNames[clanNameIndex].Substring(0, 4)
                            && ClanNames[clanNameIndex - 1].Count(c => c == ' ') == ClanNames[clanNameIndex].Count(c => c == ' '))
                            threeWords = ClanNames[clanNameIndex - 1]; // from plural to singular
                        result.Add(words[i]);
                        result.Add(words[i + 1]);
                        var lastChar = cleanWords[i + 2][cleanWords[i + 2].Length - 1];
                        if (!IsPunctuation(lastChar))
                        {
                            result.Add(words[i + 2]);
                            result.Add("[" + threeWords + "]");
                        }
                        else
                        {
                            var lastIndex = words[i + 2].Length - 1;
                            while (lastIndex >= 0 && IsPunctuation(words[i + 2][lastIndex]))
                                --lastIndex;
                            result.Add(words[i + 2].Substring(0, lastIndex + 1));
                            result.Add("[" + threeWords + "]" + words[i + 2].Substring(lastIndex + 1, words[i + 2].Length - lastIndex - 1));
                        }
                        i += 2; // skip second & third word
                        continue;
                    }
                }
                if (i < cleanWords.Length - 1)
                {
                    var twoWords = TrimPunctuation(cleanWords[i] + " " + cleanWords[i + 1]);
                    if (twoWords.EndsWith("'s"))
                        twoWords = twoWords.Substring(0, twoWords.Length - 2);
                    if (-1 != (clanNameIndex = Array.IndexOf(ClanNames, twoWords)) && !IsClanTitle(cleanWords, i + 2))
                    {
                        if (clanNameIndex > 0
                            && ClanNames[clanNameIndex - 1].Substring(0, 4) == ClanNames[clanNameIndex].Substring(0, 4)
                            && ClanNames[clanNameIndex - 1].Count(c => c == ' ') == ClanNames[clanNameIndex].Count(c => c == ' '))
                            twoWords = ClanNames[clanNameIndex - 1]; // from plural to singular
                        result.Add(words[i]);
                        var lastChar = cleanWords[i + 1][cleanWords[i + 1].Length - 1];
                        if (!IsPunctuation(lastChar))
                        {
                            result.Add(words[i + 1]);
                            result.Add("[" + twoWords + "]");
                        }
                        else
                        {
                            var lastIndex = words[i + 1].Length - 1;
                            while (lastIndex >= 0 && IsPunctuation(words[i + 1][lastIndex]))
                                --lastIndex;
                            result.Add(words[i + 1].Substring(0, lastIndex + 1));
                            result.Add("[" + twoWords + "]" + words[i + 1].Substring(lastIndex + 1, words[i + 1].Length - lastIndex - 1));
                        }
                        ++i; // skip second word
                        continue;
                    }
                }

                var oneWord = TrimPunctuation(cleanWords[i]);
                if (oneWord.EndsWith("'s"))
                    oneWord = oneWord.Substring(0, oneWord.Length - 2);
                if (-1 != (clanNameIndex = Array.IndexOf(ClanNames, oneWord)) && !IsClanTitle(cleanWords, i + 1))
                {
                    if (clanNameIndex > 0
                        && ClanNames[clanNameIndex - 1].Substring(0, 4) == ClanNames[clanNameIndex].Substring(0, 4)
                        && ClanNames[clanNameIndex - 1].Count(c => c == ' ') == ClanNames[clanNameIndex].Count(c => c == ' '))
                        oneWord = ClanNames[clanNameIndex - 1]; // from plural to singular
                    var lastChar = cleanWords[i][cleanWords[i].Length - 1];
                    if (!IsPunctuation(lastChar))
                    {
                        result.Add(words[i]);
                        result.Add("[" + oneWord + "]");
                    }
                    else
                    {
                        var lastIndex = words[i].Length - 1;
                        while (lastIndex >= 0 && IsPunctuation(words[i][lastIndex]))
                            --lastIndex;
                        result.Add(words[i].Substring(0, lastIndex + 1));
                        result.Add("[" + oneWord + "]" + words[i].Substring(lastIndex + 1, words[i].Length - lastIndex - 1));
                    }
                }
                else
                    result.Add(words[i]);
            }
            return result;
        }

        protected static bool IsSuperiorDiscipline(string discipline)
        {
            return discipline.ToUpper()[1] == discipline[1];
        }

        protected static void WriteSymbol(PdfContentByte cb, float x, float y, BaseFont symbolFont, float fontSize, char maskSymbol, char symbol, int foreColor = 0)
        {
            WriteText(cb, maskSymbol.ToString(CultureInfo.InvariantCulture), x, y, symbolFont, fontSize, 0xFFFFFF);
            WriteText(cb, symbol.ToString(CultureInfo.InvariantCulture), x, y, symbolFont, fontSize, foreColor);
        }

        protected static void WriteSymbolCentered(PdfContentByte cb, float x, float y, BaseFont symbolFont, float fontSize, char maskSymbol, char symbol, int foreColor = 0)
        {
            if (maskSymbol == '<')
                x += fontSize * 0.1f;
            WriteSymbol(cb, x, y, symbolFont, fontSize, maskSymbol, symbol, foreColor);
        }

        protected static void WriteWatermark(PdfContentByte cb, string text, float x, float y, float width, float height, BaseFont font, bool useGhost, TextOrientation orientation, int color, int watermarkOpacity)
        {
            const float halfPi = (float)Math.PI / 2f;
            const double threeHalfPi = 3f * Math.PI / 2f;
            var angle = orientation == TextOrientation.Horizontal
                ? 0
                : (float)Math.Atan(height / width); // write the text along the diagonal of the bounding box

            var scale = (float)Math.Cos(angle); // reduce the size so when rotated, the text box doesn't exceed the bounding box.

            var words = text.Split(' ').ToList();
            var fontSize = height / words.Count;

            var textWidths = words.Select(word => font.GetWidthPoint(word, fontSize)).ToList();

            var maxTextWidth = textWidths.Max();

            // try to regroup short words
            bool newGroup;
            do
            {
                newGroup = false;
                for (int i = 0; i < words.Count - 1; ++i)
                {
                    var ww = font.GetWidthPoint(words[i], fontSize);
                    if (ww == maxTextWidth)
                        continue;
                    var ww2 = font.GetWidthPoint(words[i + 1], fontSize);
                    if (ww + ww2 <= maxTextWidth)
                    {
                        words[i] = words[i] + " " + words[i + 1];
                        words.RemoveAt(i + 1);
                        --i;
                        newGroup = true;
                    }
                }
            }
            while (newGroup);
            textWidths = words.Select(word => font.GetWidthPoint(word, fontSize)).ToList(); // recompute the text widths

            var wordDistance = words.Select((w, i) => fontSize * (((words.Count - 1) / 2f) - i - 1f + BaselineFactor)).ToList(); // stack the words vertically along the vertical center line

            // rotate all points around the central point (0,0)
            var wordRelPositions = wordDistance.Select(distance => distance >= 0
                ? new { X = (float)Math.Cos(halfPi + angle) * distance, Y = (float)Math.Sin(halfPi + angle) * distance }
                : new { X = (float)Math.Cos(threeHalfPi + angle) * Math.Abs(distance), Y = (float)Math.Sin(threeHalfPi + angle) * Math.Abs(distance) })
                .ToList();

            var gstate = new PdfGState { FillOpacity = watermarkOpacity / 100f, StrokeOpacity = 0.5f };
            cb.SaveState();
            cb.SetGState(gstate);

            var xCenter = x + width / 2f;
            var yCenter = y + height / 2f;
            for (int i = 0; i < words.Count; ++i)
            {
                var word = words[i];
                cb.BeginText();
                cb.SetTextRenderingMode(PdfContentByte.TEXT_RENDER_MODE_FILL);
                cb.SetHorizontalScaling(100f * scale * width / textWidths[i]);
                cb.SetRGBColorFill((color & 0xFF0000) >> 16, (color & 0x00FF00) >> 8, color & 0x0000FF);
                //cb.SetRGBColorStroke(useGhost ? 255 : 0, useGhost ? 255 : 0, useGhost ? 255 : 0);
                //cb.SetLineWidth(fontSize / 100f);
                cb.SetFontAndSize(font, fontSize);
                cb.ShowTextAligned(Element.ALIGN_CENTER, word, xCenter + wordRelPositions[i].X, yCenter + wordRelPositions[i].Y, 180f * angle / (float)Math.PI);
                cb.EndText();
            }
            cb.RestoreState();
        }

        protected static void WriteText(PdfContentByte cb, string text,
            float x, float y,
            BaseFont font, float fontSize, int color, float scaling = 100f)
        {
            cb.BeginText();
            cb.SetTextRenderingMode(PdfContentByte.TEXT_RENDER_MODE_FILL);
            cb.SetTextMatrix(x, y);
            cb.SetHorizontalScaling(scaling);
            cb.SetRGBColorFill((color & 0xFF0000) >> 16, (color & 0x00FF00) >> 8, color & 0x0000FF);
            cb.SetFontAndSize(font, fontSize);
            cb.ShowText(text);
            cb.EndText();
        }

        protected static void WriteText(PdfContentByte cb, string text,
            float x, float y,
            BaseFont font, float fontSize, int color, int outlineColor, float scaling = 100f, float lineWidth = 0.25f)
        {
            cb.BeginText();
            cb.SetTextRenderingMode(PdfContentByte.TEXT_RENDER_MODE_FILL_STROKE);
            cb.SetTextMatrix(x, y);
            cb.SetHorizontalScaling(scaling);
            cb.SetRGBColorFill((color & 0xFF0000) >> 16, (color & 0x00FF00) >> 8, color & 0x0000FF);
            cb.SetFontAndSize(font, fontSize);
            cb.SetLineWidth(lineWidth);
            cb.SetColorStroke(new BaseColor((outlineColor & 0xFF0000) >> 16, (outlineColor & 0x00FF00) >> 8, outlineColor & 0x0000FF));
            cb.ShowText(text);
            cb.EndText();
        }

        protected static void WriteTextCentered(PdfContentByte cb, string text,
            float x, float y,
            BaseFont font, float fontSize, int color, float scaling = 100f)
        {
            var width = font.GetWidthPoint(text, fontSize);
            WriteText(cb, text, x - width / 2f, y, font, fontSize, color, scaling);
        }

        protected static void WriteRightAlignedText(PdfContentByte cb, string text,
            float x, float y,
            BaseFont font, float fontSize, int color, float scaling = 100f)
        {
            var width = font.GetWidthPoint(text, fontSize) * scaling / 100f;
            WriteText(cb, text, x - width, y, font, fontSize, color, scaling);
        }

        /// <summary>
        /// Indicates whether a circle and a rectangle intersects.
        /// </summary>
        /// <param name="xCenter">The x position of the center of the circle.</param>
        /// <param name="yCenter">The y position of the center of the circle.</param>
        /// <param name="radius">The radius of the circle.</param>
        /// <param name="rect">The rectangle.</param>
        /// <returns>True if the 2 shapes intersect, false otherwise.</returns>
        protected bool Intersects(float xCenter, float yCenter, float radius, Rectangle rect)
        {
            var circleDistancex = Math.Abs(xCenter - rect.Left - rect.Width / 2);
            var circleDistancey = Math.Abs(yCenter - rect.Bottom - rect.Height / 2);

            if (circleDistancex > (rect.Width / 2 + radius)) { return false; }
            if (circleDistancey > (rect.Height / 2 + radius)) { return false; }

            if (circleDistancex <= (rect.Width / 2)) { return true; }
            if (circleDistancey <= (rect.Height / 2)) { return true; }

            var cornerDistanceSq =
                (circleDistancex - rect.Width / 2) * (circleDistancex - rect.Width / 2) +
                (circleDistancey - rect.Height / 2) * (circleDistancey - rect.Height / 2);

            return (cornerDistanceSq <= (radius * radius));
        }

        public void DrawCutLines(ProxyDocument proxyDocument)
        {
            if (proxyDocument.ImageIndex % 9 == 0)
                return;

            var cb = proxyDocument.Writer.DirectContent;

            var cardGrid = new bool[3, 3];
            var i = -1 + proxyDocument.ImageIndex % 9;
            do
            {
                cardGrid[i % 3, i / 3] = true;
            }
            while (--i > 0);

            cb.SetLineWidth(1f);
            cb.SetRGBColorStroke(32, 32, 32);
            for (var x = 1; x < 3; ++x)
            {
                var sum = 0;
                for (i = 0; i < 3; ++i)
                    sum += cardGrid[x, i] ? 1 : 0;
                if (sum == 0)
                    continue;
                cb.MoveTo(x * CardWidthBorder + LeftPageMargin / ZoomFactor, 3 * CardHeightBorder + BottomPageMargin / ZoomFactor);
                cb.LineTo(x * CardWidthBorder + LeftPageMargin / ZoomFactor, (3 - sum) * CardHeightBorder + BottomPageMargin / ZoomFactor);
                cb.Stroke();
            }

            for (var y = 1; y < 3; ++y)
            {
                var sum = 0;
                for (i = 0; i < 3; ++i)
                    sum += cardGrid[i, y] ? 1 : 0;
                if (sum == 0)
                    continue;
                cb.MoveTo(LeftPageMargin / ZoomFactor, (3 - y) * CardHeightBorder + BottomPageMargin / ZoomFactor);
                cb.LineTo(sum * CardWidthBorder + LeftPageMargin / ZoomFactor, (3 - y) * CardHeightBorder + BottomPageMargin / ZoomFactor);
                cb.Stroke();
            }

            /*if (proxyDocument.ImageIndex % 9 != 0)
                DrawCutLines();*/
        }

        protected static void DrawCutLines(PdfContentByte cb)
        {
            cb.SetLineWidth(1f);
            cb.SetRGBColorStroke(32, 32, 32);
            for (var i = 1; i < 3; ++i)
            {
                cb.MoveTo(LeftPageMargin / ZoomFactor, i * CardHeightBorder + BottomPageMargin / ZoomFactor);
                cb.LineTo(CardWidthBorder * 3 + LeftPageMargin / ZoomFactor, i * CardHeightBorder + BottomPageMargin / ZoomFactor);
                cb.Stroke();
                cb.MoveTo(i * CardWidthBorder + LeftPageMargin / ZoomFactor, BottomPageMargin / ZoomFactor);
                cb.LineTo(i * CardWidthBorder + LeftPageMargin / ZoomFactor, 3 * CardHeightBorder + BottomPageMargin / ZoomFactor);
                cb.Stroke();
            }
        }

        protected static void GetNewCardsCoordinates(ProxyDocument proxyDocument, Document doc, out float x, out float y)
        {
            if (proxyDocument.ImageIndex % proxyDocument.CardsPerPage == 0)
                doc.NewPage();

            x = proxyDocument.CardsPerPage == 9
                ? (proxyDocument.ImageIndex % 3) * CardWidthBorder + LeftPageMargin / ZoomFactor + (CardWidthBorder - CardWidth) / 2f
                : LeftPageMargin / ZoomFactor;
            y = proxyDocument.CardsPerPage == 9
                ? (2 - (((proxyDocument.ImageIndex % 9)) / 3)) * CardHeightBorder + BottomPageMargin / ZoomFactor + (CardHeightBorder - CardHeight) / 2f
                : BottomPageMargin / ZoomFactor;
        }

        public abstract void FixCard(StreamWriter outputFile, DataRow cardRow, out string outputMessage);

        protected static string EscapeAndQuote(string value)
        {
            return "\"" + value.Replace("\"", "\"\"") + "\"";
        }
    }
}
